Bahasa Indonesia

Jelajahi tipe bermerek TypeScript, teknik canggih untuk mencapai pengetikan nominal dalam sistem tipe struktural. Pelajari cara meningkatkan keamanan tipe dan kejelasan kode.

Tipe Bermerek TypeScript: Pengetikan Nominal dalam Sistem Struktural

Sistem tipe struktural TypeScript menawarkan fleksibilitas tetapi terkadang dapat menyebabkan perilaku yang tidak terduga. Tipe bermerek (branded types) menyediakan cara untuk memberlakukan pengetikan nominal, meningkatkan keamanan tipe dan kejelasan kode. Artikel ini menjelajahi tipe bermerek secara mendetail, memberikan contoh praktis dan praktik terbaik untuk implementasinya.

Memahami Pengetikan Struktural vs. Nominal

Sebelum mendalami tipe bermerek, mari kita perjelas perbedaan antara pengetikan struktural dan nominal.

Pengetikan Struktural (Duck Typing)

Dalam sistem tipe struktural, dua tipe dianggap kompatibel jika mereka memiliki struktur yang sama (yaitu, properti yang sama dengan tipe yang sama). TypeScript menggunakan pengetikan struktural. Perhatikan contoh ini:


interface Point {
  x: number;
  y: number;
}

interface Vector {
  x: number;
  y: number;
}

const point: Point = { x: 10, y: 20 };
const vector: Vector = point; // Valid in TypeScript

console.log(vector.x); // Output: 10

Meskipun Point dan Vector dideklarasikan sebagai tipe yang berbeda, TypeScript mengizinkan penugasan objek Point ke variabel Vector karena keduanya berbagi struktur yang sama. Ini bisa jadi nyaman, tetapi juga dapat menyebabkan kesalahan jika Anda perlu membedakan antara tipe yang secara logis berbeda namun kebetulan memiliki bentuk yang sama. Sebagai contoh, bayangkan koordinat untuk lintang/bujur yang mungkin secara kebetulan cocok dengan koordinat piksel layar.

Pengetikan Nominal

Dalam sistem tipe nominal, tipe dianggap kompatibel hanya jika mereka memiliki nama yang sama. Bahkan jika dua tipe memiliki struktur yang sama, mereka diperlakukan sebagai tipe yang berbeda jika memiliki nama yang berbeda. Bahasa seperti Java dan C# menggunakan pengetikan nominal.

Kebutuhan akan Tipe Bermerek

Pengetikan struktural TypeScript bisa menjadi masalah ketika Anda perlu memastikan bahwa sebuah nilai milik tipe tertentu, terlepas dari strukturnya. Sebagai contoh, pertimbangkan representasi mata uang. Anda mungkin memiliki tipe yang berbeda untuk USD dan EUR, tetapi keduanya bisa direpresentasikan sebagai angka. Tanpa mekanisme untuk membedakannya, Anda bisa secara tidak sengaja melakukan operasi pada mata uang yang salah.

Tipe bermerek mengatasi masalah ini dengan memungkinkan Anda membuat tipe-tipe berbeda yang secara struktural serupa tetapi diperlakukan sebagai tipe yang berbeda oleh sistem tipe. Ini meningkatkan keamanan tipe dan mencegah kesalahan yang mungkin lolos begitu saja.

Mengimplementasikan Tipe Bermerek di TypeScript

Tipe bermerek diimplementasikan menggunakan tipe persimpangan (intersection types) dan simbol unik atau literal string. Idenya adalah menambahkan "merek" ke sebuah tipe yang membedakannya dari tipe lain dengan struktur yang sama.

Menggunakan Simbol (Disarankan)

Menggunakan simbol untuk penandaan merek umumnya lebih disukai karena simbol dijamin unik.


const USD = Symbol('USD');
type USD = number & { readonly [USD]: unique symbol };

const EUR = Symbol('EUR');
type EUR = number & { readonly [EUR]: unique symbol };

function createUSD(value: number): USD {
  return value as USD;
}

function createEUR(value: number): EUR {
  return value as EUR;
}

function addUSD(a: USD, b: USD): USD {
  return (a + b) as USD;
}

const usd1 = createUSD(10);
const usd2 = createUSD(20);
const eur1 = createEUR(15);

const totalUSD = addUSD(usd1, usd2);
console.log("Total USD:", totalUSD);

// Uncommenting the next line will cause a type error
// const invalidOperation = addUSD(usd1, eur1);

Dalam contoh ini, USD dan EUR adalah tipe bermerek yang didasarkan pada tipe number. unique symbol memastikan bahwa tipe-tipe ini berbeda. Fungsi createUSD dan createEUR digunakan untuk membuat nilai dari tipe-tipe ini, dan fungsi addUSD hanya menerima nilai USD. Mencoba menambahkan nilai EUR ke nilai USD akan menghasilkan kesalahan tipe.

Menggunakan Literal String

Anda juga dapat menggunakan literal string untuk penandaan merek, meskipun pendekatan ini kurang kuat dibandingkan menggunakan simbol karena literal string tidak dijamin unik.


type USD = number & { readonly __brand: 'USD' };
type EUR = number & { readonly __brand: 'EUR' };

function createUSD(value: number): USD {
  return value as USD;
}

function createEUR(value: number): EUR {
  return value as EUR;
}

function addUSD(a: USD, b: USD): USD {
  return (a + b) as USD;
}

const usd1 = createUSD(10);
const usd2 = createUSD(20);
const eur1 = createEUR(15);

const totalUSD = addUSD(usd1, usd2);
console.log("Total USD:", totalUSD);

// Uncommenting the next line will cause a type error
// const invalidOperation = addUSD(usd1, eur1);

Contoh ini mencapai hasil yang sama seperti contoh sebelumnya, tetapi menggunakan literal string alih-alih simbol. Meskipun lebih sederhana, penting untuk memastikan bahwa literal string yang digunakan untuk penandaan merek bersifat unik di dalam basis kode Anda.

Contoh Praktis dan Kasus Penggunaan

Tipe bermerek dapat diterapkan pada berbagai skenario di mana Anda perlu memberlakukan keamanan tipe di luar kompatibilitas struktural.

ID

Pertimbangkan sebuah sistem dengan berbagai jenis ID, seperti UserID, ProductID, dan OrderID. Semua ID ini mungkin direpresentasikan sebagai angka atau string, tetapi Anda ingin mencegah pencampuran tipe ID yang berbeda secara tidak sengaja.


const UserIDBrand = Symbol('UserID');
type UserID = string & { readonly [UserIDBrand]: unique symbol };

const ProductIDBrand = Symbol('ProductID');
type ProductID = string & { readonly [ProductIDBrand]: unique symbol };

function getUser(id: UserID): { name: string } {
  // ... fetch user data
  return { name: "Alice" };
}

function getProduct(id: ProductID): { name: string, price: number } {
  // ... fetch product data
  return { name: "Example Product", price: 25 };
}

function createUserID(id: string): UserID {
  return id as UserID;
}

function createProductID(id: string): ProductID {
  return id as ProductID;
}

const userID = createUserID('user123');
const productID = createProductID('product456');

const user = getUser(userID);
const product = getProduct(productID);

console.log("User:", user);
console.log("Product:", product);

// Uncommenting the next line will cause a type error
// const invalidCall = getUser(productID);

Contoh ini menunjukkan bagaimana tipe bermerek dapat mencegah pengiriman ProductID ke fungsi yang mengharapkan UserID, sehingga meningkatkan keamanan tipe.

Nilai Spesifik Domain

Tipe bermerek juga dapat berguna untuk merepresentasikan nilai-nilai spesifik domain dengan batasan. Misalnya, Anda mungkin memiliki tipe untuk persentase yang harus selalu berada di antara 0 dan 100.


const PercentageBrand = Symbol('Percentage');
type Percentage = number & { readonly [PercentageBrand]: unique symbol };

function createPercentage(value: number): Percentage {
  if (value < 0 || value > 100) {
    throw new Error('Percentage must be between 0 and 100');
  }
  return value as Percentage;
}

function applyDiscount(price: number, discount: Percentage): number {
  return price * (1 - discount / 100);
}

try {
  const discount = createPercentage(20);
  const discountedPrice = applyDiscount(100, discount);
  console.log("Discounted Price:", discountedPrice);

  // Uncommenting the next line will cause an error during runtime
  // const invalidPercentage = createPercentage(120);
} catch (error) {
  console.error(error);
}

Contoh ini menunjukkan cara memberlakukan batasan pada nilai tipe bermerek saat runtime. Meskipun sistem tipe tidak dapat menjamin bahwa nilai Percentage selalu antara 0 dan 100, fungsi createPercentage dapat memberlakukan batasan ini saat runtime. Anda juga dapat menggunakan pustaka seperti io-ts untuk memberlakukan validasi runtime pada tipe bermerek.

Representasi Tanggal dan Waktu

Bekerja dengan tanggal dan waktu bisa rumit karena berbagai format dan zona waktu. Tipe bermerek dapat membantu membedakan antara representasi tanggal dan waktu yang berbeda.


const UTCDateBrand = Symbol('UTCDate');
type UTCDate = string & { readonly [UTCDateBrand]: unique symbol };

const LocalDateBrand = Symbol('LocalDate');
type LocalDate = string & { readonly [LocalDateBrand]: unique symbol };

function createUTCDate(dateString: string): UTCDate {
  // Validate that the date string is in UTC format (e.g., ISO 8601 with Z)
  if (!/\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z/.test(dateString)) {
    throw new Error('Invalid UTC date format');
  }
  return dateString as UTCDate;
}

function createLocalDate(dateString: string): LocalDate {
  // Validate that the date string is in local date format (e.g., YYYY-MM-DD)
  if (!/\d{4}-\d{2}-\d{2}/.test(dateString)) {
    throw new Error('Invalid local date format');
  }
  return dateString as LocalDate;
}

function convertUTCDateToLocalDate(utcDate: UTCDate): LocalDate {
  // Perform time zone conversion
  const date = new Date(utcDate);
  const localDateString = date.toLocaleDateString();
  return createLocalDate(localDateString);
}

try {
  const utcDate = createUTCDate('2024-01-20T10:00:00.000Z');
  const localDate = convertUTCDateToLocalDate(utcDate);
  console.log("UTC Date:", utcDate);
  console.log("Local Date:", localDate);
} catch (error) {
  console.error(error);
}

Contoh ini membedakan antara tanggal UTC dan lokal, memastikan bahwa Anda bekerja dengan representasi tanggal dan waktu yang benar di berbagai bagian aplikasi Anda. Validasi runtime memastikan bahwa hanya string tanggal yang diformat dengan benar yang dapat diberi tipe ini.

Praktik Terbaik untuk Menggunakan Tipe Bermerek

Untuk menggunakan tipe bermerek secara efektif di TypeScript, pertimbangkan praktik terbaik berikut:

Kelebihan Tipe Bermerek

Kekurangan Tipe Bermerek

Alternatif untuk Tipe Bermerek

Meskipun tipe bermerek adalah teknik yang kuat untuk mencapai pengetikan nominal di TypeScript, ada pendekatan alternatif yang mungkin Anda pertimbangkan.

Tipe Opak (Opaque Types)

Tipe opak mirip dengan tipe bermerek tetapi menyediakan cara yang lebih eksplisit untuk menyembunyikan tipe yang mendasarinya. TypeScript tidak memiliki dukungan bawaan untuk tipe opak, tetapi Anda dapat mensimulasikannya menggunakan modul dan simbol privat.

Kelas

Menggunakan kelas dapat memberikan pendekatan yang lebih berorientasi objek untuk mendefinisikan tipe yang berbeda. Meskipun kelas diketik secara struktural di TypeScript, mereka menawarkan pemisahan masalah yang lebih jelas dan dapat digunakan untuk memberlakukan batasan melalui metode.

Pustaka seperti `io-ts` atau `zod`

Pustaka-pustaka ini menyediakan validasi tipe runtime yang canggih dan dapat digabungkan dengan tipe bermerek untuk memastikan keamanan baik pada saat kompilasi maupun saat runtime.

Kesimpulan

Tipe bermerek TypeScript adalah alat yang berharga untuk meningkatkan keamanan tipe dan kejelasan kode dalam sistem tipe struktural. Dengan menambahkan "merek" pada sebuah tipe, Anda dapat memberlakukan pengetikan nominal dan mencegah pencampuran yang tidak disengaja dari tipe yang secara struktural serupa tetapi secara logis berbeda. Meskipun tipe bermerek memperkenalkan beberapa kompleksitas dan overhead, manfaat dari peningkatan keamanan tipe dan kemudahan pemeliharaan kode sering kali lebih besar daripada kekurangannya. Pertimbangkan untuk menggunakan tipe bermerek dalam skenario di mana Anda perlu memastikan bahwa sebuah nilai milik tipe tertentu, terlepas dari strukturnya.

Dengan memahami prinsip-prinsip di balik pengetikan struktural dan nominal, dan dengan menerapkan praktik terbaik yang diuraikan dalam artikel ini, Anda dapat secara efektif memanfaatkan tipe bermerek untuk menulis kode TypeScript yang lebih kuat dan mudah dipelihara. Dari merepresentasikan mata uang dan ID hingga memberlakukan batasan spesifik domain, tipe bermerek menyediakan mekanisme yang fleksibel dan kuat untuk meningkatkan keamanan tipe dalam proyek Anda.

Saat Anda bekerja dengan TypeScript, jelajahi berbagai teknik dan pustaka yang tersedia untuk validasi dan penegakan tipe. Pertimbangkan untuk menggunakan tipe bermerek bersama dengan pustaka validasi runtime seperti io-ts atau zod untuk mencapai pendekatan yang komprehensif terhadap keamanan tipe.